﻿using Apewer;
using Apewer.Internals;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Apewer.Network
{

    /// <summary></summary>
    public class HttpClient
    {

        string _key = TextUtility.Key();
        int _block = 1024;

        /// <summary></summary>
        public string Key { get => _key; }

        /// <summary>获取进度的块大小，以字节为单位，默认值：1024（1KB）。</summary>
        public int Block { get => _block; set => NumberUtility.Restrict(value, 1, 1048576); }

        /// <summary>构建 <see cref="HttpClient"/> 的实例。</summary>
        public HttpClient()
        {
            RequestHeaders = new StringPairs();
        }

        #region request

        /// <summary>获取或设置将要请求的方法，默认为 GET。</summary>
        public HttpMethod Method { get; set; }

        /// <summary>获取或设置将要请求的地址。</summary>
        public string Url { get; set; }

        /// <summary>获取或设置请求主体的超时毫秒数，默认值取决于运行时（100,000 毫秒）。</summary>
        public int Timeout { get; set; }

        /// <summary>获取或设置请求使用的证书。</summary>
        public X509Certificate ClientCertificate { get; set; }

        /// <summary>获取或设置请求的自定义头。</summary>
        public StringPairs RequestHeaders { get; set; }

        /// <summary>获取或设置请求的 Cookies。</summary>
        public Cookie[] RequestCookies { get; set; }

        /// <summary>获取或设置 POST 请求发送的数据。当 Stream 属性有效时此属性将被忽略。</summary>
        public byte[] RequestData { get; set; }

        /// <summary>获取或设置 POST 请求发送的数据。此属性有效时将忽略 Data 属性。</summary>
        public Stream RequestStream { get; set; }

        /// <summary>获取或设置写入请求主体的进度回调。</summary>
        public Action<long> RequestProgress { get; set; }

        /// <summary>获取或设置请求的内容类型，默认为空。此属性可能会被自定义头取代。</summary>
        public string RequestContentType { get; set; }

        /// <summary>获取或设置请求的内容字节长度，此属性仅在指定大于 0 时被使用。此属性可能会被自定义头或主体字节数组取代。</summary>
        public long RequestContentLength { get; set; }

        /// <summary>获取或设置用户代理。此属性可能会被自定义头取代。</summary>
        public string UserAgent { get; set; }

        /// <summary>获取或设置来源。此属性可能会被自定义头取代。</summary>
        public string Referer { get; set; }

        #endregion

        #region response

        /// <summary>跟随响应的重定向。</summary>
        /// <remarks>默认值：False</remarks>
        public bool AllowRedirect { get; set; }

        /// <summary>获取或设置要接收响应主体的流。默认为 NULL 值。</summary>
        /// <remarks>指定为 NULL 时，响应体将写入字节数组；<br />非 NULL 时，响应体将写入此流，并忽略 ResponseData 属性。</remarks>
        public Stream ResponseStream { get; set; }

        /// <summary>获取或设置读取响应主体的进度回调</summary>
        public Action<long> ResponseProgress { get; set; }

        /// <summary>获取响应的状态。</summary>
        public string ResponseStatus { get; private set; }

        /// <summary>获取响应的缓存状态。</summary>
        public bool ResponseCached { get; private set; }

        /// <summary>获取响应的头。</summary>
        public Cookie[] ResponseCookies { get; private set; }

        /// <summary>获取响应的头。</summary>
        public StringPairs ResponseHeaders { get; private set; }

        /// <summary>获取响应的主体，当指定流时此属性将不被使用。</summary>
        public byte[] ResponseData { get; private set; }

        #endregion

        #region send

        object _locker = new object();
        Exception _exception = null;
        HttpWebRequest _request = null;
        HttpWebResponse _response = null;

        Nullable<DateTime> _request_time = null;
        Nullable<DateTime> _response_time = null;

        /// <summary>发送请求的时间。</summary>
        public Nullable<DateTime> RequestTime { get => _request_time; }

        /// <summary>接收响应的时间。</summary>
        public Nullable<DateTime> ResponseTime { get => _response_time; }

        /// <summary>获取最近发生的异常。</summary>
        public Exception Exception { get { return _exception; } }

        Exception Return(Exception ex)
        {
            _exception = ex;
            return ex;
        }

        /// <summary>发送请求，并获取响应。</summary>
        /// <param name="catchEx">捕获发生的异常，将异常作为返回值。</param>
        /// <returns>发生的异常。</returns>
        public Exception Send(bool catchEx = true)
        {
            lock (_locker)
            {
                _exception = null;

                ResponseStatus = default;
                ResponseCached = default;
                ResponseCookies = default;
                ResponseHeaders = default;
                ResponseData = default;

                var url = Url;
                if (url.IsEmpty())
                {
                    var ex = new MissingMemberException("未指定 URL。");
                    if (!catchEx) throw ex;
                    return Return(ex);
                }

                var method = Method;
                if (method == HttpMethod.NULL)
                {
                    if (RequestData != null || RequestStream != null) method = HttpMethod.POST;
                    else method = HttpMethod.GET;
                }

                if (catchEx)
                {
                    try
                    {
                        _request = Prepare(url, method);
                        _request_time = DateTime.Now;
                        _response = _request.GetResponse() as HttpWebResponse;
                        _response_time = DateTime.Now;
                        Parse(_response);
                    }
                    catch (Exception ex)
                    {
                        _response_time = DateTime.Now;
                        _exception = ex;
                        if (ex is WebException webEx) Parse((HttpWebResponse)webEx.Response);
                        return ex;
                    }
                }
                else
                {
                    _request = Prepare(url, method);
                    _request_time = DateTime.Now;
                    _response = _request.GetResponse() as HttpWebResponse;
                    _response_time = DateTime.Now;
                    Parse(_response);
                }
            }

            return null;
        }

        /// <exception cref="System.ArgumentException"></exception>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="System.InvalidOperationException"></exception>
        /// <exception cref="System.NotSupportedException"></exception>
        /// <exception cref="System.ObjectDisposedException"></exception>
        /// <exception cref="System.Security.SecurityException"></exception>
        /// <exception cref="System.Net.ProtocolViolationException"></exception>
        /// <exception cref="System.Net.WebException"></exception>
        HttpWebRequest Prepare(string url, HttpMethod method)
        {
            var https = url.ToLower().StartsWith("https");
            if (https) SslUtility.ApproveValidation();

            var request = (HttpWebRequest)WebRequest.Create(url);

            var certificate = ClientCertificate;
            if (certificate != null) request.ClientCertificates.Add(certificate);

            var timeout = Timeout;
            if (timeout > 0) request.Timeout = timeout;
            request.Method = method.ToString();
            request.AllowAutoRedirect = AllowRedirect;

            request.CookieContainer = new CookieContainer();
            var cookies = RequestCookies;
            if (cookies != null)
            {
                foreach (var cookie in cookies)
                {
                    if (cookie == null) continue;
                    request.CookieContainer.Add(cookie);
                }
            }

            var length = RequestContentLength;
            var type = RequestContentType;
            var ua = UserAgent;
            var r = Referer;
            var headers = RequestHeaders;
            if (headers != null) foreach (var header in headers)
                {
                    var key = header.Key.ToTrim(); ;
                    var value = header.Value.ToTrim();
                    if (TextUtility.IsBlank(key)) continue;
                    if (TextUtility.IsBlank(value)) continue;
                    try
                    {
                        switch (key.Lower())
                        {
                            case "accept":
                                request.Accept = value;
                                break;
                            case "connection":
                                request.Connection = value;
                                break;
                            case "content-length":
                            case "contentlength":
                                length = Math.Max(length, NumberUtility.Int64(value));
                                break;
                            case "content-type":
                            case "contenttype":
                                type = value;
                                break;
                            case "expect":
                                request.Expect = value;
                                break;
#if !NET20
                            case "host":
                                request.Host = value;
                                break;
#endif
                            case "if-modified-since":
                                var requestIfModifiedSince = ClockUtility.Parse(value);
                                if (requestIfModifiedSince != null) request.IfModifiedSince = requestIfModifiedSince.Value;
                                break;
                            case "referer":
                            case "referrer":
                                r = value;
                                break;
                            case "transfer-encoding":
                                request.TransferEncoding = value;
                                break;
                            case "user-agent":
                            case "useragent":
                                ua = value;
                                break;
                            default:
                                request.Headers.Add(header.Key, header.Value);
                                break;
                        }
                    }
                    catch { }
                }
            if (length > 0L) request.ContentLength = length;
            if (type.NotEmpty()) request.ContentType = type;
            if (ua.NotEmpty()) request.UserAgent = ua;
            if (r.NotEmpty()) request.Referer = r;

            if (method == HttpMethod.POST)
            {
                var stream = RequestStream;
                var data = RequestData;
                if (stream != null)
                {
                    var body = request.GetRequestStream();
                    BytesUtility.Read(stream, body, RequestProgress, Block);
                }
                else if (data != null)
                {
                    request.ContentLength = data.LongLength;
                    var body = request.GetRequestStream();
                    BytesUtility.Write(body, data, RequestProgress, Block);
                }
            }

            return request;
        }

        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="System.InvalidOperationException"></exception>
        /// <exception cref="System.NotSupportedException"></exception>
        /// <exception cref="System.ObjectDisposedException"></exception>
        /// <exception cref="System.Net.ProtocolViolationException"></exception>
        /// <exception cref="System.Net.WebException"></exception>
        void Parse(HttpWebResponse response)
        {
            if (response == null) return;

            ResponseStatus = ((int)response.StatusCode).ToString();
            ResponseCached = response.IsFromCache;

            var headers = new StringPairs();
            foreach (var key in response.Headers.AllKeys)
            {
                var values = response.Headers.GetValues(key);
                headers.Add(key, values.Join(","));
            }
            ResponseHeaders = headers;

            var cb = new ArrayBuilder<Cookie>();
            foreach (var item in response.Cookies)
            {
                var cookie = item as Cookie;
                if (cookie == null) continue;
                cb.Add(cookie);
            }

            var body = response.GetResponseStream();
            var progress = ResponseProgress;
            var hasProgress = progress != null;
            var stream = ResponseStream;
            if (stream == null) ResponseData = BytesUtility.Read(body);
            else BytesUtility.Read(body, stream, progress, Block);

            response.Close();
        }

        #endregion

        #region static

        const int SimpleTimeout = 30000;

        /// <summary>GET</summary>
        public static HttpClient Get(string url, int timeout = 30000)
        {
            var http = new HttpClient();
            http.Url = url;
            http.Timeout = timeout;
            http.Method = HttpMethod.GET;
            http.Send();
            return http;
        }

        /// <summary>POST</summary>
        public static HttpClient Post(string url, byte[] data, int timeout = 30000, string type = "application/octet-stream")
        {
            var http = new HttpClient();
            http.Url = url;
            http.Timeout = timeout;
            http.Method = HttpMethod.POST;
            if (!string.IsNullOrEmpty(type)) http.RequestContentType = type;
            if (data != null)
            {
                http.RequestContentLength = data.LongLength;
                http.RequestData = data;
            }
            http.Send();
            return http;
        }

        /// <summary>POST text/plain</summary>
        public static HttpClient Text(string url, string text, int timeout = 30000, string type = "text/plain")
        {
            return Post(url, TextUtility.Bytes(text), timeout, type);
        }

        /// <summary>POST application/x-www-form-urlencoded</summary>
        public static HttpClient Form(string url, IDictionary<string, string> form, int timeout = 30000)
        {
            if (form == null) return Post(url, BytesUtility.Empty, timeout, "application/x-www-form-urlencoded");

            var cache = new List<string>();
            foreach (var i in form)
            {
                var key = TextUtility.EncodeUrl(i.Key);
                var value = TextUtility.EncodeUrl(i.Value);
                cache.Add(key + "=" + value);
            }
            var text = string.Join("&", cache.ToArray());
            var data = TextUtility.Bytes(text);
            return Post(url, data, timeout, "application/x-www-form-urlencoded");
        }

        /// <summary>POST application/x-www-form-urlencoded</summary>
        public static HttpClient Form(string url, Dictionary<string, string> form, int timeout = 30000) => Form(url, form as IDictionary<string, string>, timeout);

        /// <summary>合并表单参数，不包含 Query 的 ? 符号。</summary>
        public static string MergeForm(Dictionary<string, string> form)
        {
            if (form == null) return "";
            if (form.Count < 1) return "";
            var cache = new List<string>();
            foreach (var i in form)
            {
                var key = TextUtility.EncodeUrl(i.Key);
                var value = TextUtility.EncodeUrl(i.Value);
                cache.Add(key + "=" + value);
            }
            var text = string.Join("&", cache.ToArray());
            return text;
        }

        #endregion

    }

}
